TCP/IP网络编程 —— 向套接字分配网络地址

前言

回顾一下服务器端套接字的实现流程,第一步是调用socket函数创建套接字,这个函数我们在上一篇博客已经详细讲解了;第二步则是调用bind函数分配IP地址及端口号,本篇博客就来讲讲bind函数的具体实现。

bind()

#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    -> 成功时返回0,失败时返回-1。
    * sockfd        要分配地址信息(IP地址和端口号)的套接字文件描述符。
    * myaddr        存有地址信息的结构体变量地址值。
    * addrlen       第二个结构体变量的长度。

如果此函数调用成功,则将第二个参数指定的地址信息分配给第一个参数中的相应套接字。下面先看一个服务器端常见的套接字初始化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int serv_sock;
struct sockaddr_in serv_addr;
char *serv_port = "9190";

/* 创建服务器端套接字 */
serv_sock = socket(PF_INET, SOCK_STREAM, 0);

/* 地址信息初始化 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));

/* 分配地址信息 */
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));

相信看完这段代码的你,对socket和bind函数的调用并不会有太大的疑问,但你可能对地址信息初始化部分以及bind函数参数是怎么来的会产生很大的困惑,没有关系,我们依旧慢慢道来。

地址信息的表示

一般的,我们把要使用的IP地址和端口号以结构体的形式给出定义,这里用IPv4进行讲解,围绕该结构体讨论目标地址的表示方法:

表示IPv4地址的结构体:
struct sockaddr_in
{
    sa_family        sin_family;        //地址族(Address Family)
    uint6_t          sin_port;          //16位TCP/UDP端口号
    struct in_addr   sin_addr;          //32位IP地址
    char             sin_zero[8];       //不使用
};

该结构体中提到的另一个结构体in_addr定义如下,它用来存放32位IP地址:

struct in_addr
{
    In_addr_t        s_addr;            //32位IPv4地址
};

下面介绍以上两个结构体的成员变量~

结构体sockaddr_in的成员分析

成员sin_family

每种协议族适用的地址族均不同。比如,IPv4使用4字节地址族,IPv6使用16字节地址族。

--------------------------------------------------------------
地址族(Address Family)    |              含  义
--------------------------------------------------------------
    AF_INET                |         IPv4网络协议中使用的地址族
    AF_INET6               |         IPv6网络协议中使用的地址族
    AF_LOCAL               |     本地通信中采用的UNIX协议的地址族    
--------------------------------------------------------------  

成员sin_port

该成员保存16位端口号(以网络字节序保存)。

成员sin_addr

该成员保存32位IP地址信息(也是以网络字节序保存)。

成员sin_zero

该成员无特殊含义,我们只要知道将其填充为0即可。

网络字节序与地址变换

上面我们讲到,结构体sockaddr_in的成员sin_port和sin_addr的数据是以网络字节序保存的,至于什么是网络字节序这里我不进行赘述,大家自行了解,我们只讲字节序的转换;下面介绍两个字节序转换的函数:htons()和htonl()

  • htons/htonl中的h代表主机(host)字节序;
  • htons/htonl中的n代表网络(network)字节序;
  • s指的是short,l指的是long(Linux中long类型占用4字节);
  • htons用于端口号转换,htonl用于IP地址转换;

网络地址的初始化与分配

将字符串信息转换为网络字节序的整型数

由于sockaddr_in结构体中保存的地址信息的成员为32位整型数,而我们熟悉的IP地址的表示是点分十进制的表示法。因此,在分配IP的时候,需将其表示为32位整型数,下面同样介绍两个函数,它们可以帮助我们将字符串形式的IP地址转换成32位整型数据,且它们在转换类型的同时能够进行网络字节序的转换:

#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
    ->成功时返回32位大端序整数型值,失败时返回INADDR_NONE。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
调用inet_addr函数的demo:
/* inet_addr.c */

#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
char *addr1="1.2.3.4";
char *addr2="1.2.3.256";

unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);

conv_addr=inet_addr(addr2);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
return 0;
}
#include <arpa/inet.h>
int inet_aton(const char *string, struct in_addr *addr);
    ->成功时返回1(true),失败时返回0(false)。
    * string            含有需转换的IP地址信息的字符串地址值。
    * addr              将保存转换结果的in_addr结构体变量的地址值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
调用inet_aton函数的demo:
/* inet_aton.c */

#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handling(char *message);

int main(int argc, char *argv[])
{
char *addr="127.232.124.79";
struct sockaddr_in addr_inet;

if(!inet_aton(addr,&addr_inet.sin_addr))
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n', stderr);
exit(1);
}

上面这两个函数的区别在于,若要调用inet_addr函数,需要将转换后的IP地址信息代入sockaddr_in结构体中声明的in_addr结构体变量;而inet_aton函数则不需要此过程,因为传递in_addr结构体变量地址值时,函数会自动把结果传入该结构体变量,因此第二个函数的使用频率更高。

再补充一个与inet_aton函数正好相反的函数,此函数可以把网络字节序的整数型IP地址转换为我们熟悉的字符串形式:

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr adr);
    ->成功时返回转换的字符串地址值,失败时返回-1。

总结回顾

我们再回顾一下一开始的地址信息初始化代码:

/* 地址信息初始化1 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));

再看另一种初始化方法:

/* 地址信息初始化2 */
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(serv_ip);
serv_addr.sin_port = htons(atoi(serv_port));

经过前面讲解的知识铺垫,我们一句一句来解释地址信息初始化的代码。

1. memset(&serv_addr, 0, sizeof(serv_addr));    //将每个字节初始化为0,在这里也就是将sin_zero初始化为0
2. serv_addr.sin_family = AF_INET;              //显而易见,通俗易懂
3. serv_addr.sin_port = htons(atoi(serv_port)); //atoi函数把字符串类型的值转换成整数型

最后看有差异的第三句,inet_addr(serv_ip)相信大家都好理解,我们主要看htonl(INADDR_ANY),这是个什么格式?
原来,使用inet_addr(serv_ip),每次都要输入IP地址会有些繁琐,所以可以利用常数INADDR_ANY分配服务器端的IP地址,
若采用这种方式,则可自动获取运行服务器端的计算机的IP地址,不必亲自输入。因此,服务器端中优先考虑第二种,而客户端
一般不会采用。

好啦,讲完啦,白白~